﻿using System;
using System.Security.Cryptography;
using System.Text;
using System.IO;

namespace CryptoUtils
{
    public static class CryptoUtils
    {
        #region Constants
        private const int SHA256SIZE = 32,
            AESKEYSIZE = 32,
            AESSALTSIZE = 24;
        #endregion

        #region Public Methods

        /// <summary>
        /// Encodes the input string 
        /// </summary>
        /// <param name="inputStr"></param>
        /// <returns></returns>
        public static string ComputeHash(string inputStr)
        {
            if (string.IsNullOrEmpty(inputStr))
                return string.Empty;
            byte[] resultHV = computeHash(Encoding.UTF8.GetBytes(inputStr));
            return Convert.ToBase64String(resultHV);
        }

        /// <summary>
        /// Converts the keyPhrase string to byte array and calls the
        /// actual encryption routine to encrypt the text by passing
        /// the text to be encrypted and the keyphrase converted to byte array
        /// </summary>
        /// <param name="plainText">The text to be encrypted</param>
        /// <param name="keyPhrase">The key phrase which will be used to encrypt the text</param>
        /// <returns>The encryted text as string</returns>
        public static string Encrypt(string plainText, string keyPhrase)
        {
            byte[] keyPhraseBytes = Encoding.UTF8.GetBytes(keyPhrase);
            return encrypt(plainText, keyPhraseBytes);
        }

        /// <summary>
        /// Converts the keyPhrase string to byte array and calls the
        /// actual encryption routine to encrypt the text by passing
        /// the text to be encrypted and the keyphrase converted to byte array.
        /// After encryption if embedKey=true then the keyPhrase is pre-appended to 
        /// the encrypted string in a base-64 enci=oding format and also the length
        /// of the encoded keyPhrase is appended at the start of the string as a 2 character 
        /// numerical value.
        /// </summary>
        /// <param name="plainText">The text to be encrypted</param>
        /// <param name="keyPhrase">The key phrase which will be used to encrypt the text</param>
        /// <param name="embedKey">Checking parameter whether to append the keyPhrase in the cipher.</param>
        /// <returns>The encryted text as string</returns>
        public static string Encrypt(string plainText, string keyPhrase, bool embedKey)
        {
            if (embedKey)
                return EncryptWithEmbeddedKeyPhrase(plainText, keyPhrase);
            else
                return Encrypt(plainText, keyPhrase);
        }

        /// <summary>
        /// Converts the keyPhrase string to a byte array and invokes the 
        /// actual decryption routine to perform the decryption by passing the 
        /// cipherText to be decrypted and the keyPhrase converted to byte array
        /// </summary>
        /// <param name="cipherText">The string to be decrypted</param>
        /// <param name="keyPhrase">The key with which the decryption will be done</param>
        /// <returns>The decrypted text</returns>
        public static string Decrypt(string cipherText, string keyPhrase)
        {
            byte[] keyPhraseBytes = Encoding.UTF8.GetBytes(keyPhrase);
            return decrypt(cipherText, keyPhraseBytes);
        }

        /// <summary>
        /// Converts the keyPhrase string to a byte array and invokes the 
        /// actual decryption routine to perform the decryption by passing the 
        /// cipherText to be decrypted and the keyPhrase converted to byte array
        /// which is content within the cipher Text.
        /// </summary>
        /// <param name="cipherText">The string to be decrypted</param>
        /// <returns>The decrypted text</returns>
        public static string DecryptWithEmbeddedKeyPhrase(string cipherText)
        {
            int keyLength = Convert.ToInt16(cipherText.Substring(0, 2));
            cipherText = cipherText.Remove(0, 2);

            string encryptedKeyPhrase = cipherText.Substring(0, keyLength);
            byte[] keyPhrase = Convert.FromBase64String(encryptedKeyPhrase);
            cipherText = cipherText.Remove(0, keyLength);

            return decrypt(cipherText, keyPhrase);
        }

        /// <summary>
        /// Converts the keyPhrase string to byte array and calls the
        /// actual encryption routine to encrypt the text by passing
        /// the text to be encrypted and the keyphrase converted to byte array.
        /// After encryption if embedKey=true then the keyPhrase is pre-appended to 
        /// the encrypted string in a base-64 enci=oding format and also the length
        /// of the encoded keyPhrase is appended at the start of the string as a 2 character 
        /// numerical value.
        /// </summary>
        /// <param name="plainText">The text to be encrypted</param>
        /// <param name="keyPhrase">The key phrase which will be used to encrypt the text</param>
        /// <param name="embedKey">Checking parameter whether to append the keyPhrase in the cipher.</param>
        /// <returns>The encryted text as string</returns>
        public static string EncryptWithEmbeddedKeyPhrase(string plainText, string keyPhrase)
        {
            string encryptedString = Encrypt(plainText, keyPhrase);
            byte[] keyPhraseBytes = Encoding.UTF8.GetBytes(keyPhrase);
            string encodedKey = Convert.ToBase64String(keyPhraseBytes);
            string lengthOfKey = encodedKey.Length.ToString();
            if (lengthOfKey.Length == 1)
                lengthOfKey = "0" + lengthOfKey;
            encryptedString = lengthOfKey + encodedKey + encryptedString;
            return encryptedString;
        }
        #endregion

        #region Private methods

        /// <summary>
        /// Computes the hashcode from a <c>byte</c>  array by 
        /// calling the ComputeHash function of SHA256Managed
        /// </summary>
        /// <param name="inputByteArr">A <c>byte</c> array type</param>
        /// <returns>Byte array</returns>
        private static byte[] computeHash(byte[] inputByteArr)
        {
            if (inputByteArr.Length <= 0)
                throw new ArgumentException();
            SHA256Managed shaM = new SHA256Managed();
            return shaM.ComputeHash(inputByteArr);
        }

        /// <summary>
        /// The actual encryption routine which performs the encryption
        /// </summary>
        /// <param name="plainText">The text to be encrypted</param>
        /// <param name="inpKey">The key with which the encryption will be done</param>
        /// <returns>The encrypted text as a string</returns>
        private static string encrypt(string plainText, byte[] inpKey)
        {
            byte[] aesKey = getKey(inpKey, AESKEYSIZE);
            byte[] aesIv = getNewSalt(AESSALTSIZE);

            RijndaelManaged aes = new RijndaelManaged();
            aes.KeySize = AESKEYSIZE * 8; // in bits
            aes.BlockSize = AESSALTSIZE * 8;
            MemoryStream outStream = new MemoryStream();

            CryptoStream encStream = new CryptoStream(outStream, aes.CreateEncryptor(aesKey, aesIv), CryptoStreamMode.Write);

            byte[] inpBytes = Encoding.UTF8.GetBytes(plainText);
            encStream.Write(inpBytes, 0, inpBytes.Length);
            encStream.FlushFinalBlock();

            int byteCnt = (int)outStream.Length;
            byte[] outBytes = new byte[byteCnt + AESSALTSIZE];
            outStream.Position = 0;
            outStream.Read(outBytes, 0, byteCnt);  // stuff only the encrypted data

            storeSalt(aesIv, outBytes); // add the IV to the enc data

            encStream.Close();
            outStream.Close();

            // return the cipherText as a Base64 encoded string (includes the IV)
            return Convert.ToBase64String(outBytes, 0, outBytes.Length);
        }

        /// <summary>
        /// The actual decryption routine.
        /// </summary>
        /// <param name="cipherText">The string to be decrypted</param>
        /// <param name="inpKey">The key with which the decryption will be done</param>
        /// <returns>The dcrypted string</returns>
        private static string decrypt(string cipherText, byte[] inpKey)
        {
            // the cipherText was Base64 encoded
            byte[] inpBytes = Convert.FromBase64String(cipherText);

            byte[] aesKey = getKey(inpKey, AESKEYSIZE);
            byte[] aesIv = retrieveSalt(inpBytes, AESSALTSIZE); // fetch the IV from the cipherText bytearray

            RijndaelManaged aes = new RijndaelManaged();
            aes.KeySize = AESKEYSIZE * 8; // in bits
            aes.BlockSize = AESSALTSIZE * 8;
            MemoryStream outStream = new MemoryStream();

            CryptoStream decStream = new CryptoStream(outStream, aes.CreateDecryptor(aesKey, aesIv), CryptoStreamMode.Write);

            // remember, the total bytes of cipherText includes the IV
            // we should only decrypt the encrypted data
            decStream.Write(inpBytes, 0, (inpBytes.Length - AESSALTSIZE));
            decStream.FlushFinalBlock();

            int byteCnt = (int)outStream.Length;
            byte[] outBytes = new byte[byteCnt];
            outStream.Position = 0;
            outStream.Read(outBytes, 0, byteCnt);

            decStream.Close();
            outStream.Close();

            // return the plainText
            return Encoding.UTF8.GetString(outBytes, 0, byteCnt); ;
        }

        /// <summary>
        /// Creates the secret key to be used for the symmetric algorithm
        /// </summary>
        /// <param name="inpKey">The keyphrase byte array</param>
        /// <param name="keySize"></param>
        /// <returns>The secret key <c>byte[]</c></returns>
        private static byte[] getKey(byte[] inpKey, int keySize)
        {
            // since we are using SHA256, max keysize cannot exceed that hash size
            if (inpKey.Length == 0 || keySize <= 0 || keySize > SHA256SIZE)
                throw new ArgumentException("The input key length should be greated than zero and key size should be between 0 and the hashsize as defined by SHA256SIZE!");

            byte[] sha256Hash = computeHash(inpKey);
            byte[] finalKey = new byte[keySize];
            for (int i = 0; i < keySize; i++)
                finalKey[i] = sha256Hash[i];
            return finalKey;
        }

        /// <summary>
        /// Generates the IV to be used for the symmetric algorithm based on the
        /// salt size as the input
        /// </summary>
        /// <param name="saltSize">The salt size <c>int</c></param>
        /// <returns>A byte array</returns>
        private static byte[] getNewSalt(int saltSize)
        {
            if (saltSize <= 0)
                throw new ArgumentException("Salt size must be greater that zero!");

            byte[] randBytes = new byte[saltSize];
            RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();
            rng.GetBytes(randBytes);
            return randBytes;
        }

        /// <summary>
        /// Adds the salt at the end of the encrypted data
        /// </summary>
        /// <param name="salt">The byte array representing the salt generated</param>
        /// <param name="cipherText">The encrypted byte array</param>
        private static void storeSalt(byte[] salt, byte[] cipherText)
        {
            int saltSize = salt.Length;
            int startIdx = (cipherText.Length - saltSize);

            // if total cipherText size is less than or same as the salt, there is something wrong
            if (saltSize == 0 || startIdx <= 0)
                throw new ArgumentException("Salt size cannot be zero and the cipherText size must be greater than the salt size!");

            for (int i = 0; i < saltSize; i++)
                cipherText[startIdx + i] = salt[i];
        }

        /// <summary>
        /// Retrieves the salt from the end of the original
        /// cipherText
        /// </summary>
        /// <param name="cipherText">The cipherText byte array</param>
        /// <param name="saltSize">The salt size</param>
        /// <returns></returns>
        private static byte[] retrieveSalt(byte[] cipherText, int saltSize)
        {
            // IMP: This needs to do exactly the reverse of what storeSalt does
            // storeSalt adds the salt at the very end of the original cipherText
            // so just fetch it from there
            int startIdx = (cipherText.Length - saltSize);

            // if total cipherText size is less than or same as the salt, there is something wrong
            if (saltSize == 0 || startIdx <= 0)
                throw new ArgumentException("Salt size cannot be zero and the cipherText size must be greater than the salt size!");

            byte[] salt = new byte[saltSize];
            for (int i = 0; i < saltSize; i++)
                salt[i] = cipherText[startIdx + i];
            return salt;
        }

        #endregion
    }
}
